package org.rc.loader;

import com.google.appengine.api.blobstore.BlobKey;
import com.google.appengine.api.files.AppEngineFile;
import com.google.appengine.api.files.FileReadChannel;
import com.google.appengine.api.files.FileServiceFactory;
import com.google.appengine.api.memcache.stdimpl.GCacheFactory;
import com.google.appengine.api.taskqueue.Queue;
import com.google.appengine.api.taskqueue.QueueFactory;
import com.googlecode.objectify.Key;
import com.googlecode.objectify.Objectify;
import com.googlecode.objectify.ObjectifyService;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.HashMap;
import java.util.Map;
import java.util.Scanner;

import org.rc.dao.ResultsFileDao;
import org.rc.model.Athlete;
import org.rc.model.Race;
import org.rc.model.Result;
import org.rc.model.ResultsFile;
import org.rc.parsing.ResultDetails;
import org.rc.parsing.ResultsLineParser;
import com.google.appengine.api.taskqueue.TaskOptions.Method;
import org.rc.model.Statistics;
import static com.google.appengine.api.taskqueue.TaskOptions.Builder.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.cache.Cache;
import javax.cache.CacheException;
import javax.cache.CacheFactory;
import javax.cache.CacheManager;

/**
 * Load a results file into the datastore.
 * 
 * @author dokeeffe
 */
public class ResultsFileLoader {

    private static final Logger LOGGER = LoggerFactory.getLogger(ResultsFileLoader.class);

    private ResultsFileDao resultsFileDao;

    private static final Long PROCESSING_LIMIT = 100l;
    public static final int RETRY_LIMIT = 3;
    private Cache genericCache;

    /**
     * Constructor, sets up the cache
     */
    public ResultsFileLoader() {
        resultsFileDao = new ResultsFileDao();
        Map props = new HashMap();
        props.put(GCacheFactory.EXPIRATION_DELTA, 14400);//4hrs cache expiry
        try {
            CacheFactory cacheFactory = CacheManager.getInstance().getCacheFactory();
            genericCache = cacheFactory.createCache(props);
        } catch (CacheException e) {
            LOGGER.error("error setting up cache", e);
        }
    }

    /**
     * Setter for the resultsFileDao
     * @param resultsFileDao
     */
    public void setResultsFileDao(ResultsFileDao resultsFileDao) {
        this.resultsFileDao = resultsFileDao;
    }

    /**
     * Load a set of results from a results file in the blobstore. Starting from line passed, up to the processingLimit.
     * Transactional, and will attempt retries on failure by resubmitting to the task queue
     *
     * @param rf
     * @param parser
     * @param fromLine
     * @param retry the retry attempt counter. stops retrying after 3 attempts
     * @throws LoaderException 
     */
        public void loadResults(ResultsFile rf, ResultsLineParser parser, Long fromLine, Integer retry) throws LoaderException {
        byte[] blobResults;
        if (retry==null) {
            retry = 0;
        }
        Objectify ofy = ObjectifyService.begin();
        Key rfKey = new Key<ResultsFile>(ResultsFile.class, rf.getId());
        try {
            blobResults = resultsFileDao.LoadBlobResults(rf, fromLine,PROCESSING_LIMIT);
            Scanner scanner = new Scanner(new ByteArrayInputStream(blobResults));
            Race currentlyProcessingRace = null;
            int raceCounter = 0;
            int totalResultsImported = 0;
            int raceResultsCounter = 0;
            while (scanner.hasNextLine()) {
                String line = scanner.nextLine();
                if (line.length() > 1) {
                    ResultDetails parseResult = parser.parse(line);
                    parseResult.getRace().setImportedBy(rfKey);                    
                    parseResult.getResult().setImportedBy(rfKey);                    
                    if (currentlyProcessingRace == null) {
                        currentlyProcessingRace = locateRace(parseResult.getRace(), ofy);
                    }
                    if (!parseResult.getRace().equals(currentlyProcessingRace)) {
                        LOGGER.debug("persisting race" + parseResult.getRace().getName());
                        ofy.put(parseResult.getRace()) ;
                        if(currentlyProcessingRace!=null) {
                            //update race.result count before we move on to next race
                            incrementRaceResultsCount(ofy,currentlyProcessingRace,raceResultsCounter);
                            raceResultsCounter=0;
                        }
                        raceCounter++;
                        currentlyProcessingRace = parseResult.getRace();
                    }
                    Athlete a = persistAthlete(parseResult.getAthlete(), ofy);
                    Result result = parseResult.getResult();
                    result.setAthlete(new Key<Athlete>(Athlete.class, a.getId()));
                    result.setRace(new Key<Race>(Race.class, currentlyProcessingRace.getId()));
                    ofy.async().put(result);
                    totalResultsImported++;
                    raceResultsCounter++;
                }
            }
            if(currentlyProcessingRace!=null) {
                incrementRaceResultsCount(ofy, currentlyProcessingRace, raceResultsCounter);
            }
            updateStatistics(raceCounter, totalResultsImported, ofy);
            if (blobResults.length > 0) {
                LOGGER.info("Splitting task at " + (fromLine + PROCESSING_LIMIT));
                submitNewTask(rf.getId(), fromLine + PROCESSING_LIMIT, 0);
            }
        } catch (Exception ex) {
            if(retry<RETRY_LIMIT) {
                LOGGER.error("resubmitting because of failure");
                submitNewTask(rf.getId(), fromLine,++retry);
            } else {
                throw new LoaderException("problem loading results, retried " + RETRY_LIMIT +" times but failed", ex);
            }
        }
    }

    /**
     * add the count of parsed results to the {@link Race} and save in datastore
     * @param ofy
     * @param currentlyProcessingRace
     * @param raceResultsCounter
     */
    private void incrementRaceResultsCount(Objectify ofy, Race currentlyProcessingRace, int raceResultsCounter) {
        if(currentlyProcessingRace.getNumberOfResults()!=null) {
            raceResultsCounter = raceResultsCounter + currentlyProcessingRace.getNumberOfResults();
        }
        currentlyProcessingRace.setNumberOfResults(raceResultsCounter);
        ofy.async().put(currentlyProcessingRace);
    }



    /**
     * Spawn a new task for results file upload
     * @param id
     * @param from
     * @param retry
     */
    private void submitNewTask(Long id, long from, int retry) {
        Queue queue = QueueFactory.getDefaultQueue();
        queue.add(withUrl("/parse?id=" + id + "&from=" + from + "&retry=" + retry).method(Method.GET));
    }

    /**
     * Save an {@link Athlete} in the datastore. If already there then return the one from the datastore
     * @param athlete
     * @param ofy
     * @return 
     */
    private Athlete persistAthlete(Athlete athlete, Objectify ofy) {
        Athlete found = ofy.query(Athlete.class).filter("firstName", athlete.getFirstName()).filter("secondName", athlete.getSecondName()).get();
        if (found == null) {
            ofy.put(athlete);
            found = athlete;
        }
        return found;
    }

    /**
     * Locate a race in the Datastore that matches the one passed
     * If the name and data match they are condidered the same.
     * 
     * @param race
     * @return 
     */
    private Race locateRace(Race race, Objectify ofy) {
        Race result = (Race) genericCache.get("race" + race.getName() + race.getDate());
        if(result==null) {
            result = ofy.query(Race.class).filter("name", race.getName()).filter("date", race.getDate()).get();
            if (result != null) {
                LOGGER.info("located race" + result.getName() + " in datastore");
            } else {
                LOGGER.info("could not locate race in datastore " + race.getName());
            }
            genericCache.put("race"+race.getName()+race.getDate(),result);
        }
        return result;
    }

    /**
     * Update the statistics in the datastore
     * @param raceCounter
     * @param resultCounter 
     * @param ofy 
     */
    private void updateStatistics(int raceCounter, int resultCounter, Objectify ofy) {
        Statistics stats = ofy.query(Statistics.class).get();
        if(stats==null) {
            stats = new Statistics();
            stats.setNumberOfRaces(new Long(0));
            stats.setNumberOfResults(new Long(0));
        }
        stats.setNumberOfRaces(stats.getNumberOfRaces()+raceCounter);
        stats.setNumberOfResults(stats.getNumberOfResults()+resultCounter);
        ofy.put(stats);
    }
    
}
